package gpstools.data;

import generated.tcx.AbstractSourceT;
import generated.tcx.ActivityLapT;
import generated.tcx.ActivityListT;
import generated.tcx.ActivityT;
import generated.tcx.ApplicationT;
import generated.tcx.BuildT;
import generated.tcx.DeviceT;
import generated.tcx.ExtensionsT;
import generated.tcx.HeartRateInBeatsPerMinuteT;
import generated.tcx.IntensityT;
import generated.tcx.ObjectFactory;
import generated.tcx.PositionT;
import generated.tcx.SportT;
import generated.tcx.TrackT;
import generated.tcx.TrackpointT;
import generated.tcx.TrainingCenterDatabaseT;
import generated.tcx.TriggerMethodT;
import generated.tcx.VersionT;
import generated.tcxext2.ActivityLapExtensionT;
import generated.tcxext2.ActivityTrackpointExtensionT;
import gpstools.Location;
import gpstools.track.Lap;
import gpstools.track.Track;
import gpstools.track.TrackInfo.ActivityType;
import gpstools.track.TrackPoint;
import gpstools.track.addons.Source;
import gpstools.track.addons.SourceType;

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.datatype.XMLGregorianCalendar;

public class TcxExporter implements GpsDataExporter {
	private static final String DEFAULT_CREATOR = "http://code.google.com/p/gps-tools/";
	private static final Source DEFAULT_SOURCE = new Source(DEFAULT_CREATOR, SourceType.APPLICATION);
	private static ObjectFactory factory = new ObjectFactory();
	private static generated.tcxext2.ObjectFactory extFactory = new generated.tcxext2.ObjectFactory();

	private static EnumMap<ActivityType, SportT> activityTypeMapper;

	{
		activityTypeMapper = new EnumMap<ActivityType, SportT>(ActivityType.class);
		activityTypeMapper.put(ActivityType.CYCLING, SportT.BIKING);
		activityTypeMapper.put(ActivityType.RUNNING, SportT.RUNNING);
		activityTypeMapper.put(ActivityType.OTHER, SportT.OTHER);
	}

	private List<Track> tracks = new ArrayList<Track>();
	private Source source = DEFAULT_SOURCE; 

    public TcxExporter add(Track track) {
        this.tracks.add(track);
        return this;
    }

    @Override
    public GpsDataExporter addWaypoint(Location position, String label) {
        // TODO Auto-generated method stub
        return null;
    }
    
	public void setSource(Source source) {
		this.source = source;
	}
	
	@Override
	public TcxExporter setCreator(String creator) {
	    setSource(new Source(creator));
	    return this;
	}

	public void store(OutputStream os) {
		TrainingCenterDatabaseT tcx = prepareTcxData();
		try {
			JAXBContext jc = JAXBContext.newInstance(TrainingCenterDatabaseT.class, ActivityLapExtensionT.class);
			Marshaller m = jc.createMarshaller();
			m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
			m.setProperty(
					Marshaller.JAXB_SCHEMA_LOCATION,
					"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd");
			ObjectFactory of = new ObjectFactory();

			m.marshal(of.createTrainingCenterDatabase(tcx), os);
		} catch (JAXBException e) {
			throw new RuntimeException(e);
		}
	}

	private TrainingCenterDatabaseT prepareTcxData() {
		ObjectFactory of = new ObjectFactory();
		TrainingCenterDatabaseT tcx = of.createTrainingCenterDatabaseT();
		if (!this.tracks.isEmpty()) {
			ActivityListT tcxActivityList = of.createActivityListT();
			for (Track track : this.tracks) {
				ActivityT tcxActivity = prepareTcxActivity(track);
				tcxActivityList.getActivity().add(tcxActivity);
			}
			tcx.setActivities(tcxActivityList);
		}

		tcx.setAuthor(prepareTcxCreator(source));
		return tcx;
	}

	private ActivityT prepareTcxActivity(Track track) {
		ActivityT tcxActivity = factory.createActivityT();

		List<Lap> laps = track.getLaps();
		for (Lap lap : laps) {
			tcxActivity.getLap().add(prepareTcxLap(lap));
		}
		
		tcxActivity.setId(prepareTcxTrackId(track));
		tcxActivity.setSport(activityTypeMapper.get(track.getInfo().getActivityType()));
		Source trackSource = track.getInfo().getSource() != null ? track.getInfo().getSource() : source;
		tcxActivity.setCreator(prepareTcxCreator(trackSource));

		return tcxActivity;
	}

    private static XMLGregorianCalendar prepareTcxTrackId(Track track) {
        try {
            return XMLUtils.toXMLGregorianCalendar(track.getName());
		} catch (Exception e) {
            return generateActivityId(track);
        }
    }

    private static XMLGregorianCalendar generateActivityId(Track track) {
        List<TrackPoint> points = track.getPoints();
        if (points.isEmpty()){
            return null;
        }
        Long time = points.get(0).getTime();
        return XMLUtils.toXMLGregorianCalendar(time);
    }

	private static AbstractSourceT prepareTcxCreator(Source source) {
		if (SourceType.APPLICATION.equals(source.getType())) {
			ApplicationT application = factory.createApplicationT();
			application.setName(source.getName());
			application.setPartNumber(source.getProperty("partNumber"));
			application.setLangID(source.getProperty("langId"));
			BuildT build = factory.createBuildT();
			build.setVersion(prepareTcxVersion(source));
			application.setBuild(build);
			return application;
		} else if (SourceType.DEVICE.equals(source.getType())) {
			DeviceT device = factory.createDeviceT();
			device.setName(source.getName());
			device.setProductID(toInteger(source.getProperty("productId")));
			device.setUnitId(toLong(source.getProperty("unitId")));
			device.setVersion(prepareTcxVersion(source));
			return device;
		} else {
			return null;
		}
	}

	private static VersionT prepareTcxVersion(Source source) {
		VersionT version = factory.createVersionT();
		version.setVersionMajor(toInteger(source.getProperty("version.versionMajor")));
		version.setVersionMinor(toInteger(source.getProperty("version.versionMinor")));
		version.setBuildMajor(toInteger(source.getProperty("version.buildMajor")));
		version.setBuildMinor(toInteger(source.getProperty("version.buildMinor")));
		return version;
	}

	private static long toLong(String property) {
		try {
			return Long.parseLong(property);
		} catch (NumberFormatException e) {
			return 0;
		}
	}

	private static int toInteger(String property) {
		try {
			return Integer.parseInt(property);
		} catch (NumberFormatException e) {
			return 0;
		}
	}

	private static ActivityLapT prepareTcxLap(Lap lap) {
		ActivityLapT tcxLap = factory.createActivityLapT();
		
		if (!lap.getPoints().isEmpty()) {
			tcxLap.setStartTime(XMLUtils.toXMLGregorianCalendar(lap.getPoints().get(0).getTime()));
		}
		if (lap.getInfo().getTotalTime() != null) {
			tcxLap.setTotalTimeSeconds(lap.getInfo().getTotalTime());
		}
		if (lap.getInfo().getDistance() != null) {
			tcxLap.setDistanceMeters(lap.getInfo().getDistance().doubleValue());
		}
		
		tcxLap.setMaximumSpeed(lap.getInfo().getMaximumSpeed());
		tcxLap.setCalories(lap.getInfo().getCalories() != null ? lap.getInfo().getCalories() : 0);
		tcxLap.setAverageHeartRateBpm(toTcxHeartRate(lap.getInfo().getAverageHeartRate()));
		tcxLap.setMaximumHeartRateBpm(toTcxHeartRate(lap.getInfo().getMaximumHeartRate()));
		tcxLap.setIntensity(toTcxIntensity(lap.getInfo().getString("garmin.tcx.lapIntensity")));
		tcxLap.setTriggerMethod(toTcxTriggerMethod(lap.getInfo().getString("garmin.tcx.lapTriggerMethod")));

		tcxLap.setExtensions(prepareTcxLapExtensions(lap));
		
		tcxLap.getTrack().add(prepareTcxTrackPoints(lap.getPoints()));
		
		return tcxLap;
	}

	private static TrackT prepareTcxTrackPoints(List<TrackPoint> points) {
		TrackT tcxTrackPoints = factory.createTrackT();
		for (TrackPoint point : points) {
			TrackpointT tp = factory.createTrackpointT();
			PositionT position = factory.createPositionT();
			position.setLatitudeDegrees(point.getLatitude());
			position.setLongitudeDegrees(point.getLongitude());
			tp.setPosition(position);
			tp.setAltitudeMeters(point.getElevation());
			tp.setTime(XMLUtils.toXMLGregorianCalendar(point.getTime()));
			tp.setDistanceMeters(point.getInfo().getDistanceInMeters());
			tp.setHeartRateBpm(toTcxHeartRate(point.getHeartRate()));
			
			tp.setExtensions(prepareTcxPointExtensions(point));
			
			tcxTrackPoints.getTrackpoint().add(tp);
		}
		return tcxTrackPoints;
	}

	private static ExtensionsT prepareTcxPointExtensions(TrackPoint point) {
		if (point.getPower() == null && point.getInfo().getSpeedInMetersPerSecond() == null)
			return null;
		ExtensionsT extensions = factory.createExtensionsT();
		ActivityTrackpointExtensionT tpx = extFactory.createActivityTrackpointExtensionT();

		tpx.setWatts(point.getPower());
		tpx.setSpeed(point.getInfo().getSpeedInMetersPerSecond());

		extensions.getAny().add(extFactory.createTPX(tpx));
		return extensions;
	}

	private static ExtensionsT prepareTcxLapExtensions(Lap lap) {
		if (lap.getInfo().getAverageSpeed() == null) {
			return null;
		}
		ExtensionsT extensions = factory.createExtensionsT();
		generated.tcxext2.ObjectFactory lxObjectFactory = new generated.tcxext2.ObjectFactory();
		ActivityLapExtensionT lx = lxObjectFactory.createActivityLapExtensionT();
		lx.setAvgSpeed(lap.getInfo().getAverageSpeed());
		extensions.getAny().add(lxObjectFactory.createLX(lx));
		return extensions;
	}

	private static TriggerMethodT toTcxTriggerMethod(String value) {
		if (value == null) {
			return null;
		}
		return TriggerMethodT.fromValue(value);
	}

	private static IntensityT toTcxIntensity(String value) {
		if (value == null) {
			return null;
		}
		return IntensityT.fromValue(value);
	}

	private static HeartRateInBeatsPerMinuteT toTcxHeartRate(Short heartRate) {
		if (heartRate == null) {
			return null;
		}
		HeartRateInBeatsPerMinuteT hr = factory.createHeartRateInBeatsPerMinuteT();
		hr.setValue(heartRate);
		return hr;
	}

	public void add(List<Track> tracks) {
		this.tracks.addAll(tracks);
	}

}
